Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 17 章 使用脚手架处理现有数据库

作者:Adam Freeman
翻译:陈广
日期:2019-5-13


在之前章节的示例中,首先使用 C# 类定义模型,然后用它们创建数据库,这被称为*代码行行(code-first)开发模式。对于需要使用现有数据库的项目,需要一种不同的方法,称为数据库行先行(database-first)*开发模式,在本章中,我将向您展示如何使用 Entity Framework Core 脚手架(scaffolding)功能来检查数据库并自动生成数据模型。这个功能最适合于简单的数据库,而更复杂的项目更适合手工数据建模,我在第18章中对此进行了描述。表17-1为本章简述。

表 17-1:数据库脚手架简述

问题 回答
它们是什么? 脚手架是建立数据模型的过程,这样 Entity Framework Core 就可以使用现有的数据库。
它们有何用途? 并非所有的项目都能够创建一个新的数据库。脚手架检查现有数据库并自动创建数据模型。
如何使用它们 脚托架要使用命令行工具执行
是否有任何缺陷或限制? 脚手架过程不能处理所有的数据库功能,并且可能被大型和复杂的数据库所困扰。
有没有其他选择? 您可以手动对数据库建模,如第18章所述。

表17-2为本章摘要

表 17-2:本章摘要

问题 解决方案 清单
使用脚手架处理现有数据库 运行命令行工具,然后调整 context 类,以便与 Entity Framework Core 一起使用。 1-23
在应用程序中反应数据库更改 撤销数据库 24-30

准备本章

本章依赖于在不使用实体框架核心的情况下创建的数据库,以便模拟已经存在的数据库。为此,我将使用 Visual Studio 功能来执行 SQL 查询,以创建和填充数据库,如下节所述。

注意:我一步地在下面的清单中建立数据库,以使这个过程更容易跟随。然而,输入复杂的 SQL 语句是一个容易出错的过程,创建数据库的最佳方法是下载我在本章的项目中包含的 SQL 文件,该文件可以从本书的 Github 存储库下载。https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc

了解现有数据库示例

为了帮助了解本章本部分中出现的 SQL 的来龙去脉,我将描述将要创建的数据库。数据库名称为 ZoomShoesDb,它将代表虚构的 Zoom 跑鞋公司的产品数据库。表17-3列出了我将添加到数据库中的表以及它们之间的关系。真正的数据库比示例要复杂得多,但它包含了所有我需要演示的用于使用现有数据库的 Entity Framework Core 功能的特征。

表 17-3:示例数据库中的表

名称 描述
Shoes 此表将是数据库的核心部分,并将包含公司生产的产品的详细信息。此表与所有其他表都有关系。
Categories 此表包含用于描述公司跑鞋的一组类别。它和Shoes表通过ShoeCategoryJunction表有着多对多的关系。
ShoeCategoryJunction 这是Shoes表和Categories类表之间的多对多关系的连接表.
Colors 此表包含一组颜色组合,用于鞋子,并与Shoes表有一对多的关系。
SalesCampaigns 此表包含每只鞋的销售活动的详细信息,并与鞋表有一对一的关系。

连接至数据库服务器

启动 Visual Studio 且不打开或新建项目。选择【工具】➤【New Query】➤【New Query】,并在【服务器名称】栏输入 (localdb)\MSSQLLocalDB。(注意,数据库名称中只有一个斜线 —— \字符,而不是 appsettings.json 文件中定义连接字符串时的两个。)

确保【身份验证】栏里选中Windows 身份验证,【数据库名称】栏中选中**<默认值>**,如图17-1所示。单击【连接】按钮。Visual Studio 将打开一个新的查询窗口,可以在其中输入和执行 SQL 语句。

图17-1 连接数据库服务器

创建数据库

第一步是创建数据库。在编辑窗口中输入清单17-1中的语句,右击鼠标,在弹出菜单中选择【Execute】。

注意:如果您使用的是 Visual Studio Code,在编辑面板右击鼠标的菜单中选择【Execute Query】。

清单 17-1:创建数据库

USE master

DROP DATABASE IF EXISTS ZoomShoesDb
GO

CREATE DATABASE ZoomShoesDb
GO

USE ZoomShoesDb
GO

DROP DATABASE命令在 ZoomShoesDb 数据库存在的情况下删除它,这意味着您可以返回清单,如果在为本章准备数据库时出错,可以重新开始。CREATE DATABASE命令创建数据库,USE命令告诉数据库服务器之后的命令应当应用于 ZoomShoesDb 数据库。

创建 Colors 表

表的创建顺序非常重要,因为数据库服务器不允许在不存在的表上定义外键关系。Colors 表必须在 Shoes 表之前被创建,这样 Shoes 表就可以定义一个在表17-3描述的用于一对多关系的外键列。在 SQL 编辑窗口中输入清单17-2的 SQL,鼠标右键菜单中选择【Execute】以创建并填充 Colors 表。

清单 17-2:创建并填充 Colors 表

CREATE TABLE Colors (
	Id bigint IDENTITY(1,1) NOT NULL,
	Name nvarchar(max) NOT NULL,
	MainColor nvarchar(max) NOT NULL,
	HighlightColor nvarchar(max) NOT NULL,
CONSTRAINT PK_Colors PRIMARY KEY (Id));

SET IDENTITY_INSERT dbo.Colors ON
INSERT dbo.Colors (Id, Name, MainColor, HighlightColor)
	VALUES (1, N'Red Flash', N'Red', N'Yellow'),
		(2, N'Cool Blue', N'Dark Blue', N'Light Blue'),
		(3, N'Midnight', N'Black', N'Black'),
		(4, N'Beacon', N'Yellow', N'Green')
SET IDENTITY_INSERT dbo.Colors OFF
GO

CREATE TABLE命令创建了 Colors 表,它拥有IdNameMainColor以及HighlightColor列,其中Id列作为主键列。INSERT命令填充表,SET IDENTITY命令用于临时允许向Id列手动添加值。Colors 表被配置为由数据库服务器负责生成Id列的值,但我需要在填充数据库时指定这个值,以便在表间正确建立关系。

为测试您是否正确创建了数据表,在编辑窗口中替换清单17-3中的 SQL 查询。

清单 17-3:查询 Colors 表

SELECT * FROM Colors

在编辑窗口中右击鼠标,并在弹出菜单中选择【Execute】;您将看到表17-4显示的输出,它反映了 Colors 表的结构和内容。

警告:如果没有得到预期的结果,那么返回到清单17-1并重新开始。您可能会不顾一切地继续下去,但是不会在本章后面获得预期的结果。

表 17-4:Colors 表的结构和数据

Id Name MainColor HighlightColor
1 Red Flash Red Yellow
2 Cool Blue Dark Blue Light Blue
3 Midnigh Black Black
4 Beacon Yellow Green

创建 Shoes 表

Shoes 表包含了鞋业公司生产的产品的详细信息。要创建该表,请将编辑器窗口中的文本替换为清单17-4所示的 SQL,然后右击并在弹出菜单中选择【Execute】。

清单 17-4:创建并填充 Shoes 表

CREATE TABLE Shoes (
	Id bigint IDENTITY(1,1) NOT NULL,
	Name nvarchar(max) NOT NULL,
	ColorId bigint NOT NULL,
	Price decimal(18, 2) NOT NULL,
CONSTRAINT PK_Shoes PRIMARY KEY (Id ),
CONSTRAINT FK_Shoes_Colors FOREIGN KEY(ColorId) REFERENCES dbo.Colors (Id))

SET IDENTITY_INSERT dbo.Shoes ON
INSERT dbo.Shoes (Id, Name, ColorId, Price)
	VALUES (1, N'Road Rocket', 2, 145.0000),
		(2, N'Trail Blazer', 4, 150.0000),
		(3, N'All Terrain Monster', 3, 250.0000),
		(4, N'Track Star', 1, 120.0000)
SET IDENTITY_INSERT dbo.Shoes OFF
GO

CREATE TABLE命令创建了一张表,带有IdNameColorId以及Price列。Id列为主键,ColorId列和 Colors 表的Id列之间存在外键关系。

为确保 Shoes 表已经被正确创建并填充,用清单17-5所示的查询替换编辑器窗口的内容。

清单 17-5:查询 Shoes 表

SELECT * FROM Shoes

右键单击编辑器窗口并从弹出菜单中选择【Execute】,您将看到表17-5中的输出,反映 Shoes 表的结构和内容。

表 17-5:Shoes 表的结构和数据

Id Name ColorId Price
1 Road Rocket 2 145.00
2 Trail Blazer 4 150.00
3 All Terrain Monster 3 250.00
4 Track Star 1 120.00

创建 SalesCampaigns 表

SalesCampaigns 表与 Shoes 表具有一对一的关系,并包含与每种鞋产品关联的销售活动的详细信息,其中 Shoes 表是关系中的主要实体。要创建和填充表,请用清单17-6所示的 SQL 替换编辑器的内容。在编辑器窗口中右键单击,然后从弹出菜单中选择【Execute】以创建和填充该表。

清单 17-6:SalesCampaigns 表的创建和填充

CREATE TABLE SalesCampaigns(
	Id bigint IDENTITY(1,1) NOT NULL,
	Slogan nvarchar(max) NULL,
	MaxDiscount int NULL,
	LaunchDate date NULL,
	ShoeId bigint NOT NULL,
CONSTRAINT PK_SalesCampaigns PRIMARY KEY (Id),
CONSTRAINT FK_SalesCampaigns_Shoes FOREIGN KEY(ShoeId)
	REFERENCES dbo.Shoes (Id),
INDEX IX_SalesCampaigns_ShoeId UNIQUE (ShoeId))

SET IDENTITY_INSERT dbo.SalesCampaigns ON
INSERT dbo.SalesCampaigns (Id, Slogan, MaxDiscount,
LaunchDate, ShoeId) VALUES
	(1, N'Jet-Powered Shoes for the Win!', 20, CAST(N'2019-01-01' AS Date), 1),
	(2, N'"Blaze" a Trail with Side-Mounted Flame Throwers ', 15, CAST(N'2019-05-03' AS Date), 2),
	(3, N'All Surfaces. All Weathers. Victory Guaranteed.',	5, CAST(N'2020-01-01' AS Date), 3),
	(4, N'Contains an Actual Star to Dazzle Competitors',25, CAST(N'2020-01-01' AS Date), 4)
SET IDENTITY_INSERT dbo.SalesCampaigns OFF
GO

SalesCampaigns 表拥有IdSloganMaxDiscountLauchDate以及ShoeId列。Id列用于存储主键,ShoeId是一个外键列,它存储来自 Shoes 表的Id列的值。还有一个索引,要求在Shoeid列上的值是唯一的,并执行与 Shoes 表的一对一关系。为了确保表已被正确创建和填充,请将编辑器窗口的内容替换为清单17-7所示的查询。

清单 17-7:查询 SalesCampaigns 表

select * from SalesCampaigns

右键单击编辑器窗口并从弹出菜单中选择【Execute】,您将看到表17-6中的输出,它反映了表的结构和内容。

表 17-6:SalesCampaigns 表的结构和内容

Id Slogan MaxDiscount LaunchDate ShoeId
1 Jet-Powered Shoes for the Win! 20 2019-01-01 1
2 "Blaze" a Trail with Side-Mounted Flame Throwers 15 2019-05-03 2
3 All Surfaces. All Weathers. Victory Guaranteed. 5 2020-01-01 3
4 Contains an Actual Star to Dazzle Competitors 25 2020-01-01 4

创建 Categories 和 ShoeCategoryJunction 表

为了完成数据库,我将创建Categories表和ShoeCategoryJunction表,这将允许与鞋表有多对多的关系。要创建和填充这些表,请用清单17-8所示的 SQL 替换编辑器窗口的内容。右键单击编辑器窗口并选择【Execute】以执行 SQL 命令指定的操作。

清单 17-8:创建 Categories 和 ShoeCategoryJunction 表

CREATE TABLE Categories(
	Id bigint IDENTITY(1,1) NOT NULL,
	Name nvarchar(max) NOT NULL,
CONSTRAINT PK_Categories PRIMARY KEY (id));

SET IDENTITY_INSERT dbo.Categories ON
INSERT dbo.Categories (Id, Name) VALUES
	(1, N'Road/Tarmac'), (2, N'Track'), (3, N'Trail'), (4, N'Road to Trail')
SET IDENTITY_INSERT dbo.Categories OFF
GO

CREATE TABLE ShoeCategoryJunction(
	Id bigint IDENTITY(1,1) NOT NULL,
	ShoeId bigint NOT NULL,
	CategoryId bigint NOT NULL,
CONSTRAINT PK_ShoeCategoryJunction PRIMARY KEY (Id),
CONSTRAINT FK_ShoeCategoryJunction_Categories FOREIGN KEY(CategoryId)
	REFERENCES dbo.Categories (Id),
CONSTRAINT FK_ShoeCategoryJunction_Shoes FOREIGN KEY(ShoeId)
	REFERENCES dbo.Shoes (Id))

SET IDENTITY_INSERT dbo.ShoeCategoryJunction ON
INSERT dbo.ShoeCategoryJunction (Id, ShoeId, CategoryId)
	VALUES (1, 1, 1), (2, 2, 3), (3, 2, 4), (4, 3, 1),
		(5, 3, 2), (6, 3, 3), (7, 3, 4), (8, 4, 2)
SET IDENTITY_INSERT dbo.ShoeCategoryJunction OFF
GO

Categories表有IdName列,Id列用于主键。ShoeCategoryJunction表有IdShoeIdCategoryId列,Id列用于主键,其它列用于ShoesCategorys表的外键关系。这是我在第16章中描述的处理多到多关系的相同方法。

为了确保表已被正确创建和填充,请将编辑器窗口的内容替换为清单17-9所示的查询。

清单 17-9:查询 Categories 和 ShoeCategoryJunction 表

SELECT * FROM Categories
INNER JOIN ShoeCategoryJunction
ON Categories.Id = ShoeCategoryJunction.ShoeId

在编辑器窗口中右键单击,然后从弹出菜单中选择【Execute】,您将看到表17-7中的输出,它结合了两个表的结构和内容。

表 17-7:Categories 和 ShoeCategoryJunction 表的结构和数据

Id Name Id ShoeId CategoryId
1 Road/Tarmac 1 1 1
2 Track 2 2 3
2 Track 3 2 4
3 Trail 4 3 1
3 Trail 5 3 2
3 Trail 6 3 3
3 Trail 7 3 4
4 Road to Trai 8 4 2

创建 ASP.NET Core MVC 项目

除了数据库之外,还需要一个 ASP.NET Core MVC 项目,这样我就可以演示如何在现有数据库中使用 Entity Framework Core。要创建项目,在 Visual Studio 【文件】菜单中选择【新建】➤【项目】,选择【ASP.NET Core Web 应用程序】模板,将项目名称设置为 ExistingDb,如图17-2所示。(将提示您保存 SQL 编辑器窗口的内容。只要收到每个查询的预期结果,就不再需要 SQL 语句。)单击【创建】按钮继续项目创建过程。

译者注:我使用的是 Visual Studio 2019 创建项目,跟原书插图并不相同

图17-2 创建新的 ASP.NET Core MVC 项目

确保窗体顶部菜单中的【.NET Core】和【ASP.NET Core 2.2】被选中,并选择【空项目】模板,如图17-3所示。单击【创建】按钮完成配置过程并创建一个新项目。

图17-3 配置新的 ASP.NET Core MVC 项目

添加 NuGet 工具包

译者注:本节所讲述的命令行工具包在 .NET Core 2.1 之后的版本默认情况已集成。如果使用的是此版本之后的 .NET Core,则可忽略本小节内容。

Visual Studio 为新项目添加的元包包含 ASP.NET Core MVC 和 Entity Framework Core 功能,但需要手动添加才能设置 Entity Framework Core 命令行工具。右键单击【解决方案资源管理器】中的 ExistingDb 项目,在弹出菜单中选择【编辑 ExistingDb.csproj】,添加如清单17-10所示的配置元素。

清单 17-10:ExistingDb 文件夹下的 ExistingDb.csproj 文件,添加工具包

<Project Sdk="Microsoft.NET.Sdk.Web">

	<PropertyGroup>
		<TargetFramework>netcoreapp2.0</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<Folder Include="wwwroot\" />
	</ItemGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
		<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet "Version="2.0.0" />
	</ItemGroup>

</Project>

保存文件,包将被下载和安装,为项目提供本章中的示例所需的命令行工具。

配置 ASP.NET Core 中间件和服务

空项目模板需要在Startup类中为 MVC 应用程序配置所需的中间件和服务,如清单17-11所示。

清单 17-11:ExistingDb 文件夹下的 Startup.cs 文件,配置中间件和服务

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace ExistingDb
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

我没有为 Entity Framework Core 添加任何配置语句。我将在本章后面设置处理数据库所需的服务。

添加控制器和视图并安装 Bootstrap

本章最后的准备是创建一个简单的控制器和视图,这样就可以测试应用程序,并安装 Bootstrap CSS 包,以便我可以很轻易地样式化显示给用户的 HTML 内容。我创建了 Controllers 文件夹,并向其中添加了一个名为 HomeController.cs 的类文件,其内容如清单 17-12 所示。

清单 17-12:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace ExistingDb.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

我将在本章稍后构建此控制器,但此刻已有一种 action 方法可以选择默认视图。为了创建视图,我创建了 Views/Home 文件夹,并向其添加了一个名为 Index.cshtml 的文件,其中添加了清单17-13所示的内容。

清单 17-13:Views/Home 文件夹下的 Index.cshtml 文件的内容

@{
    ViewData["Title"] = "Existing Database";
    Layout = "_Layout";
}

<h2 class="bg-info p-1 m-1 text-white">Placeholder for Data</h2>

为提供应用程序视图的共享布局,我创建了 Views/Shared 文件夹,并添加了一个名为 _Layout.cshtml 的文件,其内容如清单 17-14 所示。

清单 17-14:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewData["Title"]</title>
    <link rel="stylesheet" href="~/lib/twitter-bootstrap/css/bootstrap.min.css" />
</head>
<body>
    <div class="p-2">
        @RenderBody()
    </div>
</body>
</html>

为安装 Bootstrap CSS 包,我在 ExistingDb 项目上单击右键,在弹出菜单中选择【添加】➤【客户端库】。在【添加客户端库】窗口的【库】栏中输入“twitter-bootstrap@4.3.1”;在【目标位置】栏中输入“wwwroot/lib/twitter-bootstrap/。”单击【安装】按钮,此时在【解决方案资源管理器】中可以看到生成了一个新的文件 libman.json,打开它,其内容如清单 17-15 所示。

清单 17-15:ExistingDb 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.3.1",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

另外,请查看是否新生成了【wwwroot】➤【lib】➤【twitter-bootstrap】文件夹,如果已经生成,则表明 BootStrap 包已经成功安装。

测试示例应用程序

为了简化使用应用程序的过程,请编辑 Properties/launchSettings.json 文件,并更改其中包含的两个 URL,以便它们都指定端口 5000,如清单17-17所示。这是我将在 URL 中使用的端口,用于演示示例应用程序的不同特性。

清单 17-17:Properties 文件夹下手 launchSettings.json 文件,更改端口

{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "ExistingDb": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

在 ExistingDb 项目文件夹中使用dotnet run启动应用程序,并使用浏览器导航至 http://localhost:5000 以看到占位符内容,如图17-4所示。

图17-4 运行示例应用程序

对现有数据库使用脚手架

使用现有数据库的最简单方法是使用 Entity Framework Core 脚手架功能,它检查数据库并创建执行查询和其他数据操作所需的 context 和模型类。

即使您手动为现有数据库创建数据模型,正如我在第18章中所描述的那样,仍然值得使用脚手架功能来确保您所提供的数据库的描述是正确的。在接下来的部分中,我将向您展示如何使用脚手架功能,并解释如何使用它在 ASP.NET Core MVC 应用程序中创建的数据模型。

执行脚手架进程

脚手架进程使用清单17-10中添加到示例项目的包中包含的命令行工具执行。在 ExistingDb 项目文件夹中运行清单17-18所示的命令,检查示例数据库并生成数据模型。

提示:页面的布局使得很难显示复杂的命令,但是您必须注意将清单17-18的所有部分输入为命令提示符中的一行。

清单 17-18:对现有数据库使用脚手架

dotnet ef dbcontext scaffold "Server=(localdb)\MSSQLLocalDB;Database=ZoomShoesDb" "Microsoft.EntityFrameworkCore.SqlServer" --output-dir "Models/Scaffold" --context ScaffoldContext

基本命令是dotnet ef dbcontext scaffold,它运行脚手架进程。前两个参数是必须的,它提供要建模的数据库的连接字符串和数据库提供程序的名称。清单17-18中有两个可选参数:--output-dir参数指定由脚手架进程生成的 C# 类的存放目录(和命名空间)。--context参数指定用于访问数据库的 context 类的名称。


处理问题数据库

清单17-18中的命令生成的脚手架进程是无缝的,因为示例数据库很简单,并且是专门为本章创建的。当您构建一个真正的数据库时,密切关注dotnet ef命令的输出是很重要的,因为这是报告任何问题的地方。

有些问题只是警告,其中数据库的某些方面不能清晰地映射到 Entity framework Core,从而导致所创建的数据模型的妥协。但是,在 Entity framework Core 不知道如何继续的情况下,也可能会出现进程中断。对于这些情况,可以手动建模数据库(如第18章所述),也可以使用-table参数来选择要处理的表,并排除引起问题的表,从而限制脚手架过程的范围。


在清单17-18中,我为本章开头创建的示例数据库提供了连接字符串,并选择了 SQL 服务器提供程序。可选参数指定应将数据模型类放置在 Models/Scaffold 文件夹中,并将 context 类称为 ScaffoldContext。

命令将需要运行一段时间。完成后,您将看到解决方案资源管理器中显示了一个 Models/Scaffold 文件夹,其中包含一个类,该类表示数据库中的每个表,以及一个名为 ScaffoldContext 的 context 类。

每个由脚手架进程生成的类都遵循我在前面几章中描述的大致相同的方式。例如,以下是 Shoes.cs 文件的内容,用于表示数据库中 Shoes 表中的行数据:

using System;
using System.Collections.Generic;

namespace ExistingDb.Models.Scaffold
{
    public partial class Shoes
    {
        public Shoes()
        {
            ShoeCategoryJunction = new HashSet<ShoeCategoryJunction>();
        }

        public long Id { get; set; }
        public string Name { get; set; }
        public long ColorId { get; set; }
        public decimal Price { get; set; }

        public virtual Colors Color { get; set; }
        public virtual SalesCampaigns SalesCampaigns { get; set; }
        public virtual ICollection<ShoeCategoryJunction> ShoeCategoryJunction { get; set; }
    }
}

译者注:这里是使用 Visual Studio 2019 以及 .NET Core 2.2 生成的文件,跟原书代码并相同,增加了virtual关键字。

您可以看到这个类与前面章节中创建的类是如何匹配的。IdNameColorIdPrice属性对应于添加到 Shoes 表中的列。ColorSalesCampaignsShoeCategoryJunction属性允许导航到关联数据,这些数据对应于数据库中表之间的关系。

有一些小的差别。例如,类名为 Shoes,因为脚手架进程使用数据库表的名称。构造函数初始化ShoeCategoryJunction属性的集合,这是我在前面几章中省略的内容,它更喜欢在没有数据可用时处理空值,而不是一个空集合。

脚手架进程还创建了一个 context 类,它具有数据库中每个表的DbSet<T>属性。context 还重写了OnConfiguringOnModelCreating方法,这在前面的章节中并不是必需的。以下是 ScaffoldContext.cs 文件的内容,为了简洁起见,删除了OnModelCreating方法中的一些语句:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace ExistingDb.Models.Scaffold
{
    public partial class ScaffoldContext : DbContext
    {
        public ScaffoldContext() { }

        public ScaffoldContext(DbContextOptions<ScaffoldContext> options)
            : base(options) { }

        public virtual DbSet<Categories> Categories { get; set; }
        public virtual DbSet<Colors> Colors { get; set; }
        public virtual DbSet<SalesCampaigns> SalesCampaigns { get; set; }
        public virtual DbSet<ShoeCategoryJunction> ShoeCategoryJunction { get; set; }
        public virtual DbSet<Shoes> Shoes { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ZoomShoesDb");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // ...此处省略...

            modelBuilder.Entity<Shoes>(entity =>
            {
                entity.Property(e => e.Name).IsRequired();

                entity.Property(e => e.Price).HasColumnType("decimal(18, 2)");

                entity.HasOne(d => d.Color)
                    .WithMany(p => p.Shoes)
                    .HasForeignKey(d => d.ColorId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Shoes_Colors");
            });
        }
    }
}

这些方法中的代码提供的功能在前几章也有。OnConfiguring方法包含数据库的连接字符串,该连接字符串是在 ASP.NET Core MVC 项目中的设置文件中定义的。OnModelCreating方法包含创建数据库和数据模型类之间映射的语句,这些语句是迁移创建的快照类的一部分。我将很快将连接字符串移动到它的正常位置,并在第18章中解释建模方法中语句的用途。

在 ASP.NET Core MVC 中使用脚手架数据模型

一旦您创建了数据模型,就可以在 ASP.NET Core MVC 中使用它,只需做一些更改,我将在下面的部分中对其进行描述。

创建配置文件

第一步是创建一个配置文件,以包含脚手架进程创建 context 类所需的连接字符串(对于 ASP.NET Core MVC 应用程序不起作用)。我使用【ASP.NET 配置文件】项模板将一个名为 appsettings.json 的文件添加到项目中,并进行清单17-19所示的更改。

译者注:在 Visual Studio 2019 中创建项目后,appsettings.json 文件默认已存在,不需重新创建。

清单 17-19:ExistingDb 文件夹下的 appsettings.json 文件的内容

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=ZoomShoesDb;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "None",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  }
}

除了更改连接字符串之外,我还添加了Logging配置部分,以便 Entity Framework Core 将显示它发送给数据库服务器的查询的详细信息。日志记录更改对于使数据模型正常工作并不是必需的,但是非常有用,这样您就可以看到 Entity Framework Core 是如何与数据库一起工作的。

更新 Context 类

脚手架进程创建的 context 类必须进行修改,才能与 ASP.NET Core MVC 应用程序的其余部分一起使用。删除或注释OnConfiguring方法,并将其替换为一个构造函数,该构造函数通过依赖注入接受其配置选项,如清单17-20所示。

译者注:这里是使用 Visual Studio 2019 以及 .NET Core 2.2 生成的文件,构造函数默认已创建

清单 17-20:Models/Scaffold 文件夹下的 ScaffoldContext.cs 文件,更新 Context

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace ExistingDb.Models.Scaffold
{
    public partial class ScaffoldContext : DbContext
    {
        public ScaffoldContext(DbContextOptions<ScaffoldContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Categories> Categories { get; set; }
        public virtual DbSet<Colors> Colors { get; set; }
        public virtual DbSet<SalesCampaigns> SalesCampaigns { get; set; }
        public virtual DbSet<ShoeCategoryJunction> ShoeCategoryJunction { get; set; }
        public virtual DbSet<Shoes> Shoes { get; set; }

        //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        //{
        //    if (!optionsBuilder.IsConfigured)
        //    {
        //        optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ZoomShoesDb");
        //    }
        //}

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
           // ...此处省略...
        }
    }
}

注册中间件及服务

脚手架进程不配置 Entity Framework Core 以用于 ASP.NET Core MVC,也不将 context 类注册为依赖注入服务。为了使应用程序的不同部分协同工作,我将清单17-21所示的语句添加到Startup类中。

清单 17-21:ExistingDb 文件夹下的 Startup.cs 文件,配置应用程序

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using ExistingDb.Models.Scaffold;
using Microsoft.EntityFrameworkCore;

namespace ExistingDb
{
    public class Startup
    {
        public Startup(IConfiguration config) => Configuration = config;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            string conString = Configuration["ConnectionStrings:DefaultConnection"];
            services.AddDbContext<ScaffoldContext>(options =>
                options.UseSqlServer(conString));
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

更新控制器和视图

应用程序的其余部分已经准备好了,剩下的就是更新控制器,以便从数据库中获取数据并将其显示给用户。在清单17-22中,我向查询数据库数据的 Home 控制器添加了语句。我只打算添加一些基本功能,因为将对数据模型进行更改,并且不想通过多个 action 方法和视图来处理每个更改。

清单 17-22:Controllers 文件夹下的 HomeController.cs 文件,查询数据

using Microsoft.AspNetCore.Mvc;
using ExistingDb.Models.Scaffold;
using Microsoft.EntityFrameworkCore;

namespace ExistingDb.Controllers
{
    public class HomeController : Controller
    {
        private ScaffoldContext context;
        public HomeController(ScaffoldContext ctx) => context = ctx;
        public IActionResult Index()
        {
            return View(context.Shoes
                .Include(s => s.Color)
                .Include(s => s.SalesCampaigns)
                .Include(s => s.ShoeCategoryJunction)
                .ThenInclude(junct => junct.Category));
        }
    }
}

使用脚手架数据模型与使用迁移创建的模型是一样的。在清单17-22中,我使用 context 类定义的Shoes属性查询了数据库,并使用IncludeThenInclude方法跟踪导航属性到关联数据。

为了启用 ASP.NET Core MVC 标签助手,并提供对视图中模型类的轻松访问,我在 Views 文件夹中添加了一个名为 _ViewImports.cshtml 的文件,内容如清单17-23所示。

清单 17-23:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using ExistingDb.Models.Scaffold
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

要向用户显示数据,我将 Views/Home 文件夹下的 Index.cshtml 文件的内容替换为清单 17-24 所示的元素。

清单 17-24:Views/Home 文件夹下的 Index.cshtml 文件,显示数据

@model IEnumerable<Shoes>
@{
    ViewData["Title"] = "Existing Database";
    Layout = "_Layout";
}
<div class="container-fluid">
    <h4 class="bg-primary p-3 text-white">Zoom Shoes</h4>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
                <th>Color</th>
                <th>Slogan</th>
                <th>Categories</th>
            </tr>
        </thead>
        <tbody>
            @foreach (Shoes s in Model)
            {
                <tr>
                    <td>@s.Name</td>
                    <td>$@s.Price.ToString("F2")</td>
                    <td>@s.Color.Name</td>
                    <td>@s.SalesCampaigns?.Slogan</td>
                    <td>
                        @String.Join(", ", s.ShoeCategoryJunction
                        .Select(j => j.Category.Name))
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

使用dotnet run启动应用程序,并导航至 http://localhost:5000;您将看到一个包含数据库中数据的表格,通过使用脚手架进程创建的数据模型访问,如图17-5所示。

图17-5 使用脚手架数据模型

响应数据库更改

如果您不能单独使用现有的数据库,则可能必须响应为其他应用程序所做的更改。在接下来的部分中,我模拟了对数据库的更改,并演示了如何更新脚手架数据模型以适应应用程序中的更改。

更新数据库

要模拟数据库更改,选择【工具】➤【SQL Server】➤【New Query】,并使用本章开始处所描述的步骤连接至数据库。将清单17-25所示的 SQL 粘贴至编辑器,右键单击并在弹出菜单中选择【Execute】。

提示:如果要重置数据库,则可以执行清单17-1至17-6中的SQL语句。(如果从 GitHub 存储库下载本章的项目,这个过程会更容易,GitHub 存储库包含所需语句的 SQL 文件。)

清单 17-25:更改数据库

USE ZoomShoesDb

CREATE TABLE Fittings (
	Id bigint IDENTITY(1,1) NOT NULL,
	Name nvarchar(max) NOT NULL,
CONSTRAINT PK_Fittings PRIMARY KEY (Id));
GO

SET IDENTITY_INSERT Fittings ON
INSERT Fittings (Id, Name)
	VALUES (1, N'Narrow'),
		(2, N'Standard'),
		(3, N'Wide'),
		(4, N'Big Foot')
SET IDENTITY_INSERT Fittings OFF
GO

ALTER TABLE Shoes
	ADD FittingId bigint
ALTER TABLE Shoes
	ADD CONSTRAINT FK_Shoes_Fittings FOREIGN KEY(FittingId) REFERENCES Fittings (Id)
GO

UPDATE Shoes SET FittingId = 2
GO

SELECT * from Shoes

这些语句添加一个Fittings表,并在Shoes表上添加一个外键属性,该属性引用新表的主键列。最后一条语句查询表以显示更改的效果,并将生成表17-8中显示的数据。

表 17-8:在鞋表中添加一列的效果

Id Name ColorId Price FittingId
1 Road Rocket 2 145.00 2
2 Trail Blazer 4 150.00 2
3 All Terrain Monster 3 250.00 2
4 Track Star 1 120.00 2

这种类型的更改会给 ASP.NET Core MVC 应用程序带来问题,因为数据模型没有任何OutOfStock列的表示。对 Shoes 数据库的查询不会包含表中的所有数据,而且应用程序将无法在 Shoes 表中存储新的对象,因为OutOfStock列的必要值将丢失。

更新数据模型

更新数据模型以反映数据库中的更改意味着重复脚手架进程。在 ExistingDb 项目文件夹中运行清单17-26所示的命令,确保包含以粗体显示的两个附加参数。

清单 17-26:更新数据模型

dotnet ef dbcontext scaffold "Server=(localdb)\MSSQLLocalDB;Database=ZoomShoesDb" "Microsoft.EntityFrameworkCore.SqlServer" --output-dir "Models/Scaffold" --context ScaffoldContext --force --no-build

--force参数告诉 Entity Framework Core 使用新的数据模型类替换现有的。--no-build参数防止在脚手架进程执行之前生成项目。很容易陷入脚手架进程生成的数据模型与应用程序其他部分(例如控制器和视图)不同步的的情况。默认情况下,Entity Framework Core 尽量在搭建数据库之前构建项目,而失败的构建 —— 通常是因为属性或数据模型类被删除 —— 将阻止脚手架进程的执行。使用--no-build参数可以避免此问题,并允许您在数据库搭建好后更新应用程序的其余部分。

更新 Context 类

脚手架进程将替换 context 类,重写支持 ASP.NET Core MVC 应用程序所需的更改。当脚手架进程完成后,再次将OnConfiguring方法注释掉并添加一个接收配置选项参数的构造函数,如清单17-27所示。

译者注:.NET Core 2.2 版本脚手架生成的文件,已包含所需的构造函数,这里只需注释OnConfiguring方法即可。

清单 17-27:Models/Scaffold 文件夹下的 ScaffoldContext.cs 文件,更新 Context

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace ExistingDb.Models.Scaffold
{
    public partial class ScaffoldContext : DbContext
    {
        public ScaffoldContext()
        {
        }

        public ScaffoldContext(DbContextOptions<ScaffoldContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Categories> Categories { get; set; }
        public virtual DbSet<Colors> Colors { get; set; }
        public virtual DbSet<Fittings> Fittings { get; set; }
        public virtual DbSet<SalesCampaigns> SalesCampaigns { get; set; }
        public virtual DbSet<ShoeCategoryJunction> ShoeCategoryJunction { get; set; }
        public virtual DbSet<Shoes> Shoes { get; set; }

        //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        //{
        //    if (!optionsBuilder.IsConfigured)
        //    {
        //        optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ZoomShoesDb");
        //    }
        //}

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
           // ...此处省略...
        }
    }
}

更新控制器和视图

根据对数据库的更改,您还必须更新项目的其余部分,以确保应用程序正确工作。对于示例应用程序,这意味着更新 Home 控制器以遵循添加到Shoes类中的新导航属性,以便在其查询中包含关联数据,并更新视图以显示该数据。清单17-28显示了对控制器的更改。

清单 17-28:Controllers 文件夹下的 HomeController.cs 文件,跟随新的导航属性

using Microsoft.AspNetCore.Mvc;
using ExistingDb.Models.Scaffold;
using Microsoft.EntityFrameworkCore;

namespace ExistingDb.Controllers
{
    public class HomeController : Controller
    {
        private ScaffoldContext context;
        public HomeController(ScaffoldContext ctx) => context = ctx;
        public IActionResult Index()
        {
            return View(context.Shoes
                .Include(s => s.Color)
                .Include(s => s.SalesCampaigns)
                .Include(s => s.ShoeCategoryJunction)
                .ThenInclude(junct => junct.Category)
                .Include(s => s.Fitting));
        }
    }
}

为向用户显示附加数据,清单 17-29 向 Index.cshtml 视图的表格添加了一个列。

清单 17-29:Views/Home 文件夹下的 Index.cshtml 文件,显示附加数据

@model IEnumerable<Shoes>
@{
    ViewData["Title"] = "Existing Database";
    Layout = "_Layout";
}
<div class="container-fluid">
    <h4 class="bg-primary p-3 text-white">Zoom Shoes</h4>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
                <th>Color</th>
                <th>Slogan</th>
                <th>Categories</th>
                <th>Fitting</th>
            </tr>
        </thead>
        <tbody>
            @foreach (Shoes s in Model)
            {
                <tr>
                    <td>@s.Name</td>
                    <td>$@s.Price.ToString("F2")</td>
                    <td>@s.Color.Name</td>
                    <td>@s.SalesCampaigns?.Slogan</td>
                    <td>
                        @String.Join(", ", s.ShoeCategoryJunction
                        .Select(j => j.Category.Name))
                    </td>
                    <td>@s.Fitting.Name</td>
                </tr>
            }
        </tbody>
    </table>
</div>

要查看数据库更改的效果,使用dotnet run启动应用程序并导航至 http://localhost:5000,将产生如图17-6所示的结果。

图17-6 从更新后的数据库显示数据

添加持久数据模型功能

许多数据模型只是简单的类,作为数据集合和导航属性来表示数据库中的数据。但是一些应用程序需要数据模型类中的额外代码来执行诸如验证或合成数据库中不可用的数据等任务。每次数据库更改时重新创建数据模型都会出现问题,因为模型类会被覆盖,这意味着任何附加的逻辑都将丢失。

为了解决这个问题,Entity Framework Core 在脚手架进程中创建数据模型时创建分部类。例如,下面是 Entity Framework Core 创建的Shoes类的定义:

...
public partial class Shoes {
...

可以在多个文件中定义分部类,这允许将应用程序所需的任何附加逻辑与脚手架进程创建的文件分开定义,并确保它能够在数据库中保持不变。为了演示这是如何工作的,我创建了 Models/Logic 文件夹,并在其中添加了一个名为 Shoes.cs 的类文件,代码如清单17-30所示。

清单 17-30:Models/Logic 文件夹下的 Shoes.cs 文件的内容

namespace ExistingDb.Models.Scaffold
{
    public partial class Shoes
    {
        public decimal PriceIncTax => this.Price * 1.2m;
    }
}

一个分部类的不同部分必须在同一个命名空间中定义,这就是为什么这个类位于ExistingDb.Models.Scaffold命名空间中,即使类文件位于 Logic 文件夹中。分部类这一部分可以访问其他部分的成员,这意味着PriceIncTax属性能够读取Price属性的值。

警告:为分部类中的模型类定义附加逻辑意味着,在重新创建数据模型时,代码不会受到影响,但在数据库更改之后,您仍然必须确保代码保持有用和准确。

在清单 17-31 中,我更新了视图以显示通过清单17-30中定义的属性的数据。

清单 17-31:Views/Home 文件夹下的 Index.cshtml 文件,使用分部视图属性

<tbody>
    @foreach (Shoes s in Model)
    {
    <tr>
        <td>@s.Name</td>
        <td>$@s.PriceIncTax.ToString("F2")</td>
        <td>@s.Color.Name</td>
        <td>@s.SalesCampaigns?.Slogan</td>
        <td>
            @String.Join(", ", s.ShoeCategoryJunction
            .Select(j => j.Category.Name))
        </td>
        <td>@s.Fitting.Name</td>
    </tr>
    }
</tbody>

要查看附加逻辑的效果,请使用dotnet run启动应用程序,并导航到 http://localhost:5000,这将显示税收增加 20% 的价格,如图17-7所示。

图17-7 对附加模型逻辑使用分部类

总结

在本章中,我向您展示了如何使用 Entity Framework Core 脚手架功能来处理现有数据库,该功能检查数据库并创建使用它所需的 context 和实体类。对于简单的数据库来说,这是一个有用的功能,但是在更复杂的项目中使用它可能会更加困难。在下一章中,我将向您展示如何手动创建数据模型,而不是依赖于脚手架进程。

;

© 2018 - IOT小分队文章发布系统 v0.3